# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import os
import subprocess
import threading
import time

import pytest

import ansys.fluent.core as pyfluent
from ansys.fluent.core.docker.utils import get_grpc_launcher_args_for_gh_runs
from ansys.fluent.core.examples import download_file
from ansys.fluent.core.fluent_connection import (
    WaitTypeError,
    _pid_exists,
    get_container,
)
from ansys.fluent.core.launcher.error_handler import IpPortNotProvided
from ansys.fluent.core.utils.execution import asynchronous, timeout_loop
from docker.models.containers import Container


def _read_case(session, lightweight_setup=True):
    case_path = download_file("Static_Mixer_main.cas.h5", "pyfluent/static_mixer")
    session.file.read(
        file_name=case_path, file_type="case", lightweight_setup=lightweight_setup
    )


def test_session_starts_transcript_by_default(new_solver_session) -> None:
    session = new_solver_session

    def print_transcript(transcript):
        print_transcript.called = True
        if transcript:
            print_transcript.transcript = transcript

    print_transcript.called = False
    print_transcript.transcript = None

    session.transcript.register_callback(print_transcript)

    _read_case(session=session)

    assert print_transcript.called
    assert print_transcript.transcript


def test_session_starts_no_transcript_if_disabled(
    new_solver_session,
) -> None:
    session = new_solver_session
    session.transcript.stop()

    def print_transcript(transcript):
        print_transcript.called = True

    print_transcript.called = False

    session.transcript.start(write_to_stdout=False)

    _read_case(session=session)

    assert not print_transcript.called


def test_server_exits_when_session_goes_out_of_scope() -> None:
    def f():
        grpc_kwds = get_grpc_launcher_args_for_gh_runs()
        session = pyfluent.launch_fluent(**grpc_kwds)
        session.settings
        _fluent_host_pid = session.connection_properties.fluent_host_pid
        _cortex_host = session.connection_properties.cortex_host
        _inside_container = session.connection_properties.inside_container
        return _fluent_host_pid, _cortex_host, _inside_container

    fluent_host_pid, cortex_host, inside_container = f()

    timeout_loop(
        lambda: (inside_container and not get_container(cortex_host))
        or (not inside_container and not _pid_exists(fluent_host_pid)),
        60,
    )

    if inside_container:
        assert not get_container(cortex_host)
    else:
        assert not _pid_exists(fluent_host_pid)


def test_server_does_not_exit_when_session_goes_out_of_scope() -> None:
    def f():
        grpc_kwds = get_grpc_launcher_args_for_gh_runs()
        session = pyfluent.launch_fluent(cleanup_on_exit=False, **grpc_kwds)
        session.settings
        _fluent_host_pid = session.connection_properties.fluent_host_pid
        _cortex_host = session.connection_properties.cortex_host
        _inside_container = session.connection_properties.inside_container
        _cortex_pwd = session.connection_properties.cortex_pwd
        return _fluent_host_pid, _cortex_host, _inside_container, _cortex_pwd

    fluent_host_pid, cortex_host, inside_container, cortex_pwd = f()
    time.sleep(10)
    if inside_container:
        assert get_container(cortex_host)
        subprocess.Popen(["docker", "stop", cortex_host])  # cortex_host = container_id
    else:
        from pathlib import Path

        assert _pid_exists(fluent_host_pid)
        if os.name == "nt":
            cleanup_file_ext = "bat"
            cmd_list = []
        elif os.name == "posix":
            cleanup_file_ext = "sh"
            cmd_list = ["bash"]
        else:
            raise Exception("Unrecognized operating system.")
        cleanup_file_name = (
            f"cleanup-fluent-{cortex_host}-{fluent_host_pid}.{cleanup_file_ext}"
        )
        print(f"cleanup_file_name: {cleanup_file_name}")
        cmd_list.append(Path(cortex_pwd, cleanup_file_name))
        print(f"cmd_list: {cmd_list}")
        subprocess.Popen(
            cmd_list,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )


@pytest.mark.fluent_version(">=25.1")
def test_does_not_exit_fluent_by_default_when_connected_to_running_fluent(
    monkeypatch,
) -> None:
    session1 = pyfluent.launch_fluent(insecure_mode=True)

    with pytest.raises(IpPortNotProvided):
        session2 = pyfluent.connect_to_fluent(
            ip=session1.connection_properties.ip,
            password=session1.connection_properties.password,
        )

    session2 = pyfluent.connect_to_fluent(
        ip=session1.connection_properties.ip,
        port=session1.connection_properties.port,
        password=session1.connection_properties.password,
    )
    assert session2.is_server_healthy()
    session2.exit()

    timeout_loop(
        session1.is_server_healthy(),
        5.0,
        expected="truthy",
    )

    assert session1.is_server_healthy()
    session1.exit()


@pytest.mark.fluent_version(">=25.1")
def test_exit_fluent_when_connected_to_running_fluent(
    monkeypatch,
) -> None:  # import ansys.fluent.core as pyfluent
    session1 = pyfluent.launch_fluent(cleanup_on_exit=False, insecure_mode=True)
    session2 = pyfluent.connect_to_fluent(
        ip=session1.connection_properties.ip,
        port=session1.connection_properties.port,
        password=session1.connection_properties.password,
        cleanup_on_exit=True,
    )
    session2.exit()

    timeout_loop(
        session1.is_server_healthy(),
        5.0,
        expected="falsy",
    )

    assert not session1.is_server_healthy()


def test_fluent_connection_properties(
    new_solver_session,
) -> None:
    connection_properties = new_solver_session.connection_properties
    assert isinstance(connection_properties.ip, str)
    assert isinstance(connection_properties.port, int)
    assert isinstance(connection_properties.password, str)
    assert isinstance(connection_properties.cortex_pwd, str)
    assert isinstance(connection_properties.cortex_pid, int)
    assert isinstance(connection_properties.cortex_host, str)
    assert isinstance(connection_properties.inside_container, (bool, Container))
    assert isinstance(connection_properties.fluent_host_pid, int)


def test_fluent_freeze_kill(
    new_solver_session_wo_exit,
) -> None:
    session = new_solver_session_wo_exit
    _read_case(session=session, lightweight_setup=False)

    def _freeze_fluent(s):
        try:
            s.tui.mesh.modify_zones.sep_face_zone_face("interior--fluid", "yes")
        except RuntimeError as expected_tui_error_from_force_exit:
            return expected_tui_error_from_force_exit
        return

    tmp_thread = threading.Thread(target=_freeze_fluent, args=(session,), daemon=True)
    tmp_thread.start()
    tmp_thread.join(5)
    if tmp_thread.is_alive():
        session.exit(timeout=1, timeout_force=True)
        tmp_thread.join()
    else:
        raise Exception("Test should have temporarily frozen Fluent, but did not.")

    assert session.wait_process_finished(wait=5)


@pytest.mark.fluent_version(">=23.1")
def test_interrupt(static_mixer_case_session):
    solver = static_mixer_case_session
    solver.setup.general.solver.time = "unsteady-2nd-order"
    solver.solution.initialization.standard_initialize()
    asynchronous(solver.solution.run_calculation.dual_time_iterate)(
        time_step_count=100, max_iter_per_step=20
    )
    time.sleep(5)
    solver.solution.run_calculation.interrupt()
    assert solver.scheme.eval("(rpgetvar 'time-step)") < 100


def test_fluent_exit(monkeypatch: pytest.MonkeyPatch):
    import ansys.fluent.core as pyfluent

    monkeypatch.setattr(pyfluent.config, "logging_level_default", False)
    monkeypatch.setattr(pyfluent.config, "watchdog_debug", False)
    inside_container = pyfluent.config.launch_fluent_container

    grpc_kwds = get_grpc_launcher_args_for_gh_runs()
    solver = pyfluent.launch_fluent(start_watchdog=False, **grpc_kwds)
    cortex = (
        solver.connection_properties.cortex_host
        if inside_container
        else solver.connection_properties.cortex_pid
    )
    solver.exit()
    assert timeout_loop(
        lambda: (inside_container and not get_container(cortex))
        or (not inside_container and not _pid_exists(cortex)),
        timeout=60,
        idle_period=1,
    )


def test_fluent_exit_wait():
    grpc_kwds = get_grpc_launcher_args_for_gh_runs()
    session1 = pyfluent.launch_fluent(**grpc_kwds)
    fl_connection1 = session1._fluent_connection
    session1.exit()
    assert not fl_connection1.wait_process_finished(wait=0)

    session2 = pyfluent.launch_fluent(**grpc_kwds)
    session2.exit(wait=60)
    assert session2.wait_process_finished(wait=0)

    session3 = pyfluent.launch_fluent(**grpc_kwds)
    session3.exit(wait=True)
    assert session3.wait_process_finished(wait=0)

    with pytest.raises(WaitTypeError):
        session4 = pyfluent.launch_fluent(**grpc_kwds)
        session4.exit(wait="wait")


def test_wait_process_finished():
    grpc_kwds = get_grpc_launcher_args_for_gh_runs()
    meshing_session = pyfluent.launch_fluent(mode="meshing", **grpc_kwds)
    assert len(dir(meshing_session)) > 2
    assert meshing_session.is_active()
    assert meshing_session.tui
    meshing_session.exit()
    assert dir(meshing_session) == ["is_active", "wait_process_finished"]
    assert not meshing_session.is_active()
    with pytest.raises(AttributeError):
        meshing_session.tui
    assert meshing_session.wait_process_finished(wait=5)

    solver_session = pyfluent.launch_fluent(**grpc_kwds)
    assert len(dir(solver_session)) > 2
    assert solver_session.is_active()
    assert solver_session.settings
    solver_session.exit()
    assert dir(solver_session) == ["is_active", "wait_process_finished"]
    assert not solver_session.is_active()
    with pytest.raises(AttributeError):
        solver_session.settings
    assert solver_session.wait_process_finished(wait=5)
